﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Fintech.Utils;
using ZOVNP.Model.Abstracts;

namespace ZOVNP.Engine
{
        /// <summary> Посетитель-транслятор дерева выражения. </summary>
    /// <remarks> Используется для трансляции лямбда-выражений с внешними типами (интерфейсными) в лямбда-выражения с типами реализации. </remarks>
    internal class LambdaExpressionAdapter : ExpressionVisitorAdapter
    {
        #region Поля

        /// <summary> Типы входных параметров для транслируемого лямбда-выражения. </summary>
        private Type[] lambdaTypes;

        #endregion

        #region Конструкторы

        /// <summary> Создать транслятор. </summary>
        /// <param name="LambdaParameterTypes"> Типы входных параметров для транслируемого лямбда-выражения. </param>
        public LambdaExpressionAdapter(params Type[] LambdaParameterTypes)
        {
            if (LambdaParameterTypes.Length == 0 || LambdaParameterTypes.Length > 16)
                throw new ArgumentException(Properties.Resources.MSG_TemplateParametersOutOfRange, "LambdaParameterTypes");
            lambdaTypes = new Type[LambdaParameterTypes.Length + 1];
            Array.Copy(LambdaParameterTypes, lambdaTypes, LambdaParameterTypes.Length);
        }

        #endregion

        #region Методы

        /// <summary> Создание типа, задающего делегат для лямбда-выражения. </summary>
        /// <param name="ReturnType"> Тип возвращаемых делегатом данных. </param>
        /// <returns> Закрытый шаблонный тип делегата. </returns>
        private Type MakeLambdaDelegateType(Type ReturnType)
        {
            lambdaTypes[lambdaTypes.Length - 1] = ReturnType;
            switch (lambdaTypes.Length)
            {
                case 2:  return typeof(Func<,>).MakeGenericType(lambdaTypes);
                case 3:  return typeof(Func<,,>).MakeGenericType(lambdaTypes);
                case 4:  return typeof(Func<,,,>).MakeGenericType(lambdaTypes);
                case 5:  return typeof(Func<,,,,>).MakeGenericType(lambdaTypes);
                case 6:  return typeof(Func<,,,,,>).MakeGenericType(lambdaTypes);
                case 7:  return typeof(Func<,,,,,,>).MakeGenericType(lambdaTypes);
                case 8:  return typeof(Func<,,,,,,,>).MakeGenericType(lambdaTypes);
                case 9:  return typeof(Func<,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 10: return typeof(Func<,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 11: return typeof(Func<,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 12: return typeof(Func<,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 13: return typeof(Func<,,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 14: return typeof(Func<,,,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 15: return typeof(Func<,,,,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 16: return typeof(Func<,,,,,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                case 17: return typeof(Func<,,,,,,,,,,,,,,,,>).MakeGenericType(lambdaTypes);
                default: throw new ArgumentException(Properties.Resources.MSG_TemplateParametersOutOfRange, "LambdaParameterTypes");
            }
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            var lambdaType = MakeLambdaDelegateType(node.ReturnType);
            var expr = Expression.Lambda(lambdaType, node.Body, node.Name, node.TailCall, node.Parameters);
            //TODO: следует найти public API и заменить на него
            var method = typeof(LambdaExpressionAdapter).GetMethod("VisitLambdaBase", BindingFlags.NonPublic | BindingFlags.Instance);
            return (Expression)method.MakeGenericMethod(lambdaType).Invoke(this, new[] { expr });
        }

        /// <summary> Для корректного вызова метода <c>VisitLambda</c> через base. </summary>
        /// <typeparam name="T"> Тип делегата лямбда-выражения. </typeparam>
        /// <param name="node"> Само лямбда-выражение. </param>
        /// <returns> Выражение. </returns>
        /// <remarks> Вызывается через рефлексию в методе <c>LambdaExpressionAdapter.VisitLambda(...)</c>. </remarks>
        private Expression VisitLambdaBase<T>(Expression<T> node) { return base.VisitLambda(node); }

        /// <summary> Переопределение трансляции узлов типа Convert(...). </summary>
        /// <param name="node"> Узел дерева выражения. </param>
        /// <returns> Либо тот же узел дерева, либо внутренний узел узла Convert(...). </returns>
        /// <remarks> В некоторых случаях узел типа Convert(...) нужно просто исключать из результирующего дерева выражения. </remarks>
        protected override Expression VisitUnary(UnaryExpression node)
        {
            if (node.NodeType != ExpressionType.Convert) return base.VisitUnary(node);
            // Convert(...) произошёл из-за обращения к свойству из базовых интерфейсов, а этим свойством может быть только ID 
            if (node.Type == typeof(IDomainObject) || node.Type == typeof(IKeyable)) return Visit(node.Operand);
            var toMemberExpr = node.Operand as MemberExpression;
            if (toMemberExpr == null) return base.VisitUnary(node);
            var prop = toMemberExpr.Member as PropertyInfo;
            if (prop == null) return base.VisitUnary(node);
            if (!prop.PropertyType.IsEnum) return base.VisitUnary(node);
            // Для свойств-перечислений используется специальная трансляция. 
            return Visit(node.Operand);
        }

        /// <summary>  Переопределение трансляции узлов типа <c>MemberExpression</c>, то есть <c>FieldExpression</c> и <c>PropertyExpression</c>. </summary>
        /// <param name="node"> Узел дерева выражения. </param>
        /// <returns> Преобразованный узел дерева выражения. </returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        protected override Expression VisitMember(MemberExpression node)
        {
            var prop = node.Member as PropertyInfo;
            if (prop != null)
            {
                var mapped = GetPropertyMapping(prop);
                if (mapped is EnumMappingAttribute) //TODO: [Максимов Иван] [Flags()] enum support
                {
                    var enumMapped = (EnumMappingAttribute)mapped; // сам атрибут
                    var enumMask = enumMapped.MakeMask(); // значение маски (Byte/UInt16/UInt32/UInt64)
                    Type implType;
                    var casted = node.Expression;
                    if (casted is ParameterExpression && ZOVNPMetadata.TypeMapping.TryGetForwardValue(((ParameterExpression)casted).Type, out implType))
                        casted = Expression.TypeAs(node.Expression, implType);
                    else if (casted is MemberExpression && ZOVNPMetadata.TypeMapping.TryGetForwardValue(((MemberExpression)casted).Type, out implType))
                        casted = Expression.TypeAs(node.Expression, implType);
                    var exprAnd = Expression.And(Expression.Property(casted, enumMapped.PropertyName), Expression.Constant(enumMask, enumMask.GetType()));
                    if (enumMapped.StartPosition != 0) // смещение значения перечисления вправо
                        exprAnd = Expression.Divide(exprAnd, Expression.Constant(enumMapped.MakeShift(), enumMask.GetType()));
                    return Visit(exprAnd);
                }
                else if (mapped is BooleanMappingAttribute)
                {
                    var boolMapped = (BooleanMappingAttribute)mapped; // сам атрибут
                    Type implType;
                    var casted = node.Expression;
                    if (casted is ParameterExpression && ZOVNPMetadata.TypeMapping.TryGetForwardValue(((ParameterExpression)casted).Type, out implType))
                        casted = Expression.TypeAs(node.Expression, implType);
                    else if (casted is MemberExpression && ZOVNPMetadata.TypeMapping.TryGetForwardValue(((MemberExpression)casted).Type, out implType))
                        casted = Expression.TypeAs(node.Expression, implType);
                    var exprLeft = Expression.And(Expression.Property(casted, boolMapped.PropertyName), Expression.Constant(boolMapped.BooleanMask, boolMapped.BooleanMask.GetType()));
                    return boolMapped.IsFlagsMask ? // режим сопоставления - все биты или только некоторые
                        Visit(Expression.NotEqual(exprLeft, Expression.Constant(boolMapped.Zero, boolMapped.BooleanMask.GetType()))) :
                        Visit(Expression.Equal(exprLeft, Expression.Constant(boolMapped.BooleanMask, boolMapped.BooleanMask.GetType())));
                }
                return TryTranslate(node);
            }
            return TryTranslate(node);
        }

        /// <summary> Преобразовать выражение. </summary>
        /// <param name="node"> Выражение для получения свойства или поля. </param>
        /// <returns> Преобразованное выражение. </returns>
        /// <remarks> 
        /// Исправляет ошибку трансляции null-значений в оператор языка IS NULL, когда вместо него используется 
        /// обычный бинарный оператор (=, например) вместе с параметром, значение которого NULL.
        /// </remarks>
        private Expression TryTranslate(MemberExpression node)
        {
            //HACK: убрать с выходом EF vNext
            bool result = (node.GetType().Name == "FieldExpression"    && node.Expression is ConstantExpression && ((FieldInfo)node.Member).GetValue(((ConstantExpression)node.Expression).Value) == null) ||
                          (node.GetType().Name == "PropertyExpression" && node.Expression is ConstantExpression && ((PropertyInfo)node.Member).GetValue(((ConstantExpression)node.Expression).Value, null) == null);
            if (result)
                return Expression.Constant(null, node.Type);
            else
                return base.VisitMember(node);
        }

        protected override Expression VisitBinary(BinaryExpression node)
        {
            var left = Visit(node.Left);
            var right = Visit(node.Right);
            Type lt = left.Type, rt = right.Type;
            if (lt == rt)
                return node.Update(left, VisitAndConvert<LambdaExpression>(node.Conversion, "VisitBinary"), right);
            if (!lt.IsAssignableFrom(rt))
                left = Expression.MakeUnary(ExpressionType.Convert, left, rt);
            else if (!rt.IsAssignableFrom(lt))
                right = Expression.MakeUnary(ExpressionType.Convert, right, lt);
            else
                throw new InvalidCastException(string.Format(Properties.Resources.MSG_InvalidBinaryOperation, node.NodeType, lt, rt));
            return node.Update(left, VisitAndConvert<LambdaExpression>(node.Conversion, "VisitBinary"), right);
        }

        #endregion
    }

    /// <summary> Базовый класс для посетителя-транслятора дерева выражения. </summary>
    /// <remarks>
    /// Используется для размещения статических словарей трансляции, так как в в шаблонном классе - наследнике они бы
    /// создавались для каждого типа шаблонного параметра, что вызывало бы напрасные выделения памяти.
    /// </remarks>
    internal class ExpressionVisitorAdapter : ExpressionVisitor
    {
        #region Поля

        /// <summary> Словарь метаданных для трансляции свойств различных типов. </summary>
        private static Dictionary<Type, Dictionary<string, Attribute>> propertiesMappingOfTypes = new Dictionary<Type, Dictionary<string, Attribute>>();
        /// <summary> Объект для блокировки словаря с метаданными. </summary>
        private static object dictionaryLock = new object();

        #endregion

        #region Методы
        
        /// <summary> Получить информацию о том, как следует транслировать обращение к заданному свойству. </summary>
        /// <param name="Property"> Свойство, к которому происходит обращение. </param>
        /// <returns> Атрибут с информацией о трансляции, либо null, если особой трансляции не предусмотрено. </returns>
        protected static Attribute GetPropertyMapping(PropertyInfo Property)
        {
            // Свойства базовых интерфейсов не должны вызывать кеширования трансляционных свойств, иначе будет ошибка,
            // вызванная тем, что Property.ReflectedType вернёт именно базовый интерфейс, в котором ничего нет. Таким
            // образом, кеши трансляций инициализируются, но останутся пустыми и последующая трансляция работать не будет. 
            // По аналогичной причине кеширование трансляционных свойств не должно вызываться при проходе через
            // транзитные свойства, например, свойство Catalog из цепочки обращения вроде it.TableNSI.Catalog.ID.
            var reflectedType = Property.ReflectedType;
            if (reflectedType == typeof(Fintech.Utils.IKeyable) || Property.PropertyType.SupportsInterface(typeof(IDomainObject))/* || !reflectedType.SupportsInterface(typeof(IDomainObject)) зачем? */) return null;
            lock (dictionaryLock)
            {
                Dictionary<string, Attribute> typeMapping;
                if (!propertiesMappingOfTypes.TryGetValue(reflectedType, out typeMapping))
                {
                    typeMapping = new Dictionary<string, Attribute>();
                    foreach (var prop in reflectedType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
                    {
                        var attrs = prop.GetCustomAttributes(typeof(EnumMappingAttribute), true);
                        if (attrs != null && attrs.Length != 0)
                            typeMapping.Add(prop.Name, ((Attribute)attrs[0]));
                        else
                        {
                            attrs = prop.GetCustomAttributes(typeof(BooleanMappingAttribute), true);
                            if (attrs != null && attrs.Length != 0)
                                typeMapping.Add(prop.Name, ((Attribute)attrs[0]));
                        }
                    }
                    propertiesMappingOfTypes.Add(reflectedType, typeMapping);
                }
                Attribute mappingAttr;
                typeMapping.TryGetValue(Property.Name, out mappingAttr);
                return mappingAttr;
            }
        }

        #endregion
    }
}
